﻿namespace Viz
{
    using System;
    using System.Collections.Generic;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Graphics;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using System.Windows.Media.Imaging;
    using System.Threading;

    public abstract class Processing : Grid
    {
        #region Support

        protected DrawEventArgs                           EVENT;
        protected Size                                    SIZE;
        protected float                                   ASPECT_RATIO = 1f;
        protected BasicEffect                             EFFECT;
        protected Microsoft.Xna.Framework.Color           FILL = new Color(128, 128, 128);
        protected Microsoft.Xna.Framework.Color           STROKE = new Color(255, 255, 255);

        protected Stack<Matrix>                           m_views = new Stack<Matrix>();
        protected Matrix                                  m_current = Matrix.Identity;

        protected double                                  X = 0.0;
        protected double                                  Y = 0.0;
        protected double                                  Z = 0.0;

        protected double                                  SECONDS = 0;
        protected double                                  MILLISECONDS = 0;

        protected DrawingSurface                          canvas;

        protected Dictionary<TextCreateInfo, TextDrawInfo>
                                                          _textCache = new Dictionary<TextCreateInfo,TextDrawInfo>();

        public Processing()
        {
            canvas = new DrawingSurface();

            canvas.HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch;
            canvas.VerticalAlignment   = System.Windows.VerticalAlignment.Stretch;

            canvas.Loaded      += OnCanvasLoaded;
            canvas.SizeChanged += OnCanvasSizeChanged;
            canvas.Draw        += OnCanvasDraw;

            canvas.CompositionMode = new OffscreenCompositionMode()
            {
                PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8,
                PreferredMultiSampleCount   = 8,
                RenderTargetUsage           = RenderTargetUsage.DiscardContents
            };

            Children.Add(canvas);
        }

        private void OnCanvasSizeChanged(
            object                                      sender, 
            SizeChangedEventArgs                        e
        )
        {
            SIZE         = e.NewSize;
            ASPECT_RATIO = (float) (SIZE.Width / SIZE.Height);

            invalidate(false);
        }

        private void OnCanvasLoaded(
            object                                      sender, 
            System.Windows.RoutedEventArgs              e
        )
        {
            if (GraphicsDeviceManager.Current.RenderMode != RenderMode.Hardware)
            {
                MessageBox.Show("Please enable hardware acceleration.");
            }
            setup();
            invalidate(false);
        }

        private void OnCanvasDraw(
            object                                      sender, 
            DrawEventArgs                               e
        )
        {
            EVENT        = e;
            SECONDS      = EVENT.TotalTime.TotalSeconds;
            MILLISECONDS = EVENT.TotalTime.TotalMilliseconds;
            EFFECT       = new BasicEffect(device);

            draw();
        }

        #endregion

        public virtual void setup()
        {
        }

        public abstract void draw();

        public void view()
        {
            EFFECT            = new BasicEffect(device);
            EFFECT.World      = Matrix.CreateTranslation((float) X, (float) Y, (float) Z);
            EFFECT.View       = Matrix.CreateLookAt(new Vector3(0f, 0f, 5f), Vector3.Zero, Vector3.Up);
            EFFECT.Projection = Matrix.CreatePerspectiveFieldOfView((float) (Math.PI / 4), device.Viewport.AspectRatio, 1f, 100.0f);
            m_current         = Matrix.Identity;
        }

        #region Transformations

        public void translate(float x, float y, float z)
        {
            Matrix m = Matrix.CreateTranslation(x, y, z);
            m_current *= m;
        }

        public void rotate(double radX, double radY, double radZ)
        {
            Matrix x = Matrix.CreateRotationX((float) radX) * Matrix.CreateRotationY((float) radY) * Matrix.CreateRotationZ((float) radZ);
            m_current *= x;
        }

        public void rotateY(double rads)
        {
            Matrix x = Matrix.CreateRotationY((float) rads);
            m_current *= x;
        }

        public void rotateX(double rads)
        {
            Matrix x = Matrix.CreateRotationX((float) rads);
            m_current *= x;
        }

        public void rotateZ(double rads)
        {
            Matrix x = Matrix.CreateRotationZ((float) rads);
            m_current *= x;
        }

        #endregion 

        public void light(float value = 1f)
        {
            EFFECT.LightingEnabled = true;                                             // turn on the lighting subsystem.
            
            EFFECT.DirectionalLight0.DiffuseColor  = new Vector3(value, value, value); // a red light
            EFFECT.DirectionalLight0.Direction     = new Vector3(0, value, value); // coming along the x-axis
            EFFECT.DirectionalLight0.SpecularColor = new Vector3(value, value, value); // with green highlights
            EFFECT.DirectionalLight0.Enabled       = true;
            EFFECT.VertexColorEnabled              = true;

            RasterizerState rs = new RasterizerState(); 
            rs.CullMode = CullMode.None; 
            device.RasterizerState = rs; 

            //RenderState.CullFace mode, setting it to None. Again, note the warning: the "back" side of a triangle will not be properly lit, because the normal points in the wrong way.

            EFFECT.SpecularColor     = new Vector3(value, value, value);
            EFFECT.AmbientLightColor = new Vector3(value, value, value);
            EFFECT.EmissiveColor     = new Vector3(value, value, value);
            EFFECT.DiffuseColor      = new Vector3(value, value, value);

            apply();
        }

        public void apply()
        {
            EFFECT.CurrentTechnique.Passes[0].Apply();
        }

        public void push()
        {
            m_views.Push(m_current);
        }

        public void pop()
        {
            if (m_views.Count > 0)
            {
                m_current = m_views.Pop();
            }
        }

        #region Draw Points

        public void draw(params float[] p)
        {
            VertexPositionColor[] points = new VertexPositionColor[p.Length / 3];
            for (int i = 0; i < p.Length; i += 3)
            {
                points[i / 3] = vector3c(p[i], p[i + 1], p[i + 2]);
            }
            draw(points);
        }

        public void tdraw(params float[] p)
        {
            VertexPositionNormalTexture[] points = new VertexPositionNormalTexture[p.Length / 3];
            for (int i = 0; i < p.Length; i += 3)
            {
                points[i / 3] = new VertexPositionNormalTexture(vector(p[i], p[i + 1], p[i + 2]), Vector3.Forward, Vector2.Zero);
            }
            draw(points);
        }

        public void draw<T>(T[] vertices) where T: struct
        {
            VertexBuffer buf = new VertexBuffer(device, typeof(T), vertices.Length, BufferUsage.WriteOnly);

            buf.SetData<T>(0, vertices, 0, vertices.Length, 0);
            device.SetVertexBuffer(buf);
            device.DrawPrimitives(PrimitiveType.TriangleList, 0, vertices.Length / 3);
        }

        public Vector3 vector(double x, double y, double z)
        {
            return Vector3.Transform(new Vector3((float) x, (float) y, (float) z), m_current);
        }

        public VertexPositionColor vector3c(double x, double y, double z)
        {
            Vector3 v = Vector3.Transform(new Vector3((float) x, (float) y, (float) z), m_current);
            return new VertexPositionColor(v, FILL);
        }

        #endregion 

        #region Fill & Stroke

        public void stroke(System.Windows.Media.Color color)
        {
            STROKE = new Color((float) (color.R / 255.0), (float) (color.G / 255.0), (float) (color.B / 255.0));
        }

        public void stroke(float r, float g, float b)
        {
            STROKE = new Color(r, g, b);
        }

        public Color Stroke
        {
            get
            {
                return STROKE;
            }
            set
            {
                STROKE = value;
            }
        }

        public void fill(System.Windows.Media.Color color)
        {
            FILL = new Color((float) (color.R / 255.0), (float) (color.G / 255.0), (float) (color.B / 255.0));
        }

        public void fill(float r, float g, float b)
        {
            FILL = new Color(r, g, b);
        }

        public Color Fill
        {
            get
            {
                return FILL;
            }
            set
            {
                FILL = value;
            }
        }

        #endregion

        public void invalidate(bool onDraw = true)
        {
            if (EVENT != null && onDraw)
            {
                EVENT.InvalidateSurface();
            }
            else
            {
                canvas.Invalidate();
            }
        }

        public VertexPositionColor[] plot(
            VertexPositionColor[]                       vpc
        )
        {
            VertexPositionColor[] arr = new VertexPositionColor[vpc.Length];
            for (int i = 0; i < vpc.Length; ++i)
            {
                arr[i] = vpc[i];
                arr[i].Position = Vector3.Transform(arr[i].Position, m_current);
            }

            var buffer = new VertexBuffer(device, VertexPositionColor.VertexDeclaration, arr.Length, BufferUsage.WriteOnly);
            buffer.SetData(0, arr, 0, arr.Length, 0);

            device.SetVertexBuffer(buffer);
            device.DrawPrimitives(PrimitiveType.TriangleList, 0, buffer.VertexCount / 3);
            return vpc;
        } 

        public void box(System.Windows.Media.Color c)
        {
            var                   p = Shapes.Box(c.ToColor());
            VertexPositionColor[] vpc = new VertexPositionColor[p.Length];

            for (int i = 0; i < p.Length; ++i)
            {
                vpc[i] = p[i];
                var pos = p[i].Position;

                vpc[i].Position = vector(pos.X, pos.Y, pos.Z);
            }
            draw(vpc);
        }

        public VertexPositionColor[] plot(            
            float                                       x1, 
            float                                       x2, 
            float                                       y1, 
            float                                       y2, 
            int                                         count, 
            Func<float, float, float>                   function,
            bool                                        mirror = false
        )
        {
            return plot(Plot.Create(x1, x2, y1, y2, count, function, mirror));
        }

        public GraphicsDevice device
        {
            get
            {
                return GraphicsDeviceManager.Current.GraphicsDevice;
            }
        }

        #region Background

        public void background(System.Windows.Media.Color color)
        {
            device.Clear(new Color((float) (color.R / 255.0), (float) (color.G / 255.0), (float) (color.B / 255.0)));
        }

        public void background(float r, float g, float b)
        {
            var c = new Color(r, g, b);
            device.Clear(c);
        }

        #endregion

        #region Text

        public TextDrawInfo text(
            string                                      t,
            int                                         size = 24,
            string                                      fontName = "Portable User Interface"
        )
        {
            return text(t, STROKE, FILL, size, fontName);
        }

        public TextDrawInfo text(
            string                                      t,
            System.Windows.Media.Color                  fg,
            System.Windows.Media.Color                  bg,
            int                                         size = 24,
            string                                      fontName = "Portable User Interface"
        )
        {
            return text(t, fg.ToColor(), bg.ToColor(), size, fontName);
        }

        public TextDrawInfo text(
            string                                      t,
            Color                                       fg,
            Color                                       bg,
            int                                         size = 24,
            string                                      fontName = "Portable User Interface"
        )
        {
            TextDrawInfo                                info;
            TextCreateInfo                              tci = new TextCreateInfo()
            {
                Background = bg,
                Foreground = fg,
                FontName = fontName,
                Size = size,
                Text = t
            };

            if (_textCache.Count > 1000)
            {
                _textCache.Clear();
            }

            if (_textCache.ContainsKey(tci))
            {
                info = _textCache[tci];
            }
            else
            {
                info         = new TextDrawInfo();
                info.Texture = ShaderTools.Instance.GetTexture(this, t, size, fg.ToColor(), bg.ToColor(), fontName);

                float w = (float) ((info.Texture.Width / SIZE.Width) / 2);
                float h = (float) ((info.Texture.Height / SIZE.Height) / 2);

                Vector3 topLeft     = new Vector3(-w,  h, 0f);
                Vector3 bottomLeft  = new Vector3(-w, -h, 0f);
                Vector3 topRight    = new Vector3( w,  h, 0f);
                Vector3 bottomRight = new Vector3( w, -h, 0f);

                var vertices = new VertexPositionTexture[6];

                vertices[0] = new VertexPositionTexture(topRight,    TextureUV.TopRight);
                vertices[1] = new VertexPositionTexture(bottomLeft,  TextureUV.BottomLeft);
                vertices[2] = new VertexPositionTexture(topLeft,     TextureUV.TopLeft);
                vertices[3] = new VertexPositionTexture(topRight,    TextureUV.TopRight);
                vertices[4] = new VertexPositionTexture(bottomRight, TextureUV.BottomRight);
                vertices[5] = new VertexPositionTexture(bottomLeft,  TextureUV.BottomLeft);

                info.Vertices = vertices;

                _textCache.Add(tci, info);
            }
            draw(info);
            return info;
        }

        public void draw(TextDrawInfo info)
        {
            VertexPositionTexture[]                     data;
            VertexPositionTexture[]                     p;

            EFFECT.Texture            = info.Texture;
            EFFECT.TextureEnabled     = true;
            EFFECT.VertexColorEnabled = false;
            apply();

            try
            {
                p    = info.Vertices;
                data = new VertexPositionTexture[info.Vertices.Length];

                for (int i = 0; i < p.Length; ++i)
                {
                    var pos = p[i].Position;

                    data[i] = p[i];
                    data[i].Position = vector(pos.X, pos.Y, pos.Z);
                }

                var buff = new VertexBuffer(device, VertexPositionTexture.VertexDeclaration, data.Length, BufferUsage.WriteOnly);
                buff.SetData(0, data, 0, data.Length, 0);
                device.SetVertexBuffer(buff);

                device.SamplerStates[0] = SamplerState.LinearClamp;
                device.DrawPrimitives(PrimitiveType.TriangleList, 0, buff.VertexCount / 3);
            }
            finally
            {
                EFFECT.Texture            = null;
                EFFECT.TextureEnabled     = false;
                EFFECT.VertexColorEnabled = true;
                apply();
            }
        }

        #endregion
    }
}
